<?php

/**
 * @see Zend_Db
 */
require_once 'Zend/Db.php';


class Myzen_Db_Shard extends Zend_Db
{
	
/**
 * shardsTopology contains the settings for the different shards
 * @var array (num => array(Dbconfigs))
 */
    protected $_shardsTopology = array();



/**
 * In order avoiding loading too many objects, databases are loaded when called first time and stored for later use
 * @var array (shardNum => array( dbNum => DbAdapter))
 */
	protected $_loadedDbs = array();

	/**
     * Mapping of the plugins name with the shard area where it is stored
     *
     * @var array
     */
	protected $_ShardsMapping = array();

/**
 * do we enable the profiling of database queries
 * @var bool
 */
    protected $_profileEnabled = false;

/**
 * where do we log the profiling
 * @var string
 */
    protected $_profilerType = 'log';
    
    static protected $_allowedProfiler = array('firebug','log');
    
/**
 * Store for the profiler
 * @var object
 */    
	protected $_profiler = NULL;


	
	 /**
     * Constructor.
     *
     * Supported params for $config are:

     * @param  mixed $config Array of user-specified config options
     * @return void
     */
     
    public function  __construct($config = array())
    {
        if ($config instanceof Zend_Config) {
            $shards = $config->toArray();
        }

		//Check if we should profile databases
		if(isset($shards['Profile'])  && in_array  ( $shards['Profile'] , self::$_allowedProfiler)) {
			$this->_profileEnabled = true;
			$this->_profilerType = $shards['Profile'];
	    	Zend_Registry::get('dt_log')->log('Profiling enabled for dbs '.$shards['Profile'], Zend_Log::DEBUG);
	    	
		}

        $topology = $shards['topology'];

        //We just check if the default db is provided and the number of shards for the different plugins
        $shardAreaNum = 0;

		//Default shard must be provided even if never used
		if(empty($topology['default'])) throw new Myzen_Db_Exception('default Db not provided in topology');
		
		$this->_shardsTopology[$shardAreaNum] = $topology['default']['db'];
		
		$shardAreaNum++;

//Issue if only one module, there is no num in front
if(isset($topology['module']['names'])) $moduleTopology[0] = $topology['module'];
else $moduleTopology = $topology['module'];



       	foreach($moduleTopology as $num => $moduleData) {
       		
       		$moduleNames = array();
       		$controllerNames = array();
       		
       		//We get the name of the module
       		//Multiple modules may share same db configuration
       		if(empty($moduleData['names'])) throw new Myzen_Db_Exception('module hasn\'t got names attribute');
       		$moduleNames = preg_split('/,/',$moduleData['names']);
    		
       		//Module may have one or more db controllers
       		if(isset($moduleData['controller']) && is_array($moduleData['controller'])) {
       			
            //Issue if only one controller, there is no num in front
            if(isset($moduleData['controller']['names'])) $controllerTopology[0] = $moduleData['controller'];
            else $controllerTopology = $moduleData['controller'];      			

       			foreach($controllerTopology as $num => $controllerData) {
    				
       				//We get the name of the controller
       				if(empty($controllerData['names'])) throw new Myzen_Db_Exception('controller hasn\'t got names attribute');
       				$controllerNames = preg_split('/,/',$controllerData['names']);
       				//we save the topology for the shard in the shardsTopology Variable
  		     		if(empty($controllerData['db'])) throw new Myzen_Db_Exception('db not defined for controllers '.implode(',',$controllerNames));
  		     		if(isset($controllerData['db']['adapter'])) $this->_shardsTopology[$shardAreaNum][0] = $controllerData['db'];
       				else $this->_shardsTopology[$shardAreaNum] = $controllerData['db'];
       				
       				//We save the different index to the shard
       				foreach($moduleNames as $moduleName) {
       					foreach($controllerNames as $controllerName) {
       						$this->_ShardsMapping[$moduleName][$controllerName] = $shardAreaNum;	
       					}//end each controller Name 	
       				} //end each module name
       				$shardAreaNum++;
       			} //end each controller	
       			
       		} //end controller mode
       		//else the module hasn t get a controller and a db is defined for the whole module
       		else { 	
  		      if(empty($moduleData['db'])) throw new Myzen_Db_Exception('db not defined for controllers '.implode(',',$moduleNames));
				
				//If only one Db, we still need to give it the number 0
  		     	if(isset($moduleData['db']['adapter'])) $this->_shardsTopology[$shardAreaNum][0] = $moduleData['db'];
       			else $this->_shardsTopology[$shardAreaNum] = $moduleData['db'];
       		  //We save the different index to the shard
       		  foreach($moduleNames as $moduleName) {
       			$this->_ShardsMapping[$moduleName]['*'] = $shardAreaNum;	
       		  } //end each module name       		  
       		  	
       		  $shardAreaNum++;
       		} //end else module hasn t got controllers
		} // each end modules

//Zend_Registry::get('dt_log')->log($this->_shardsTopology, Zend_Log::DEBUG);
//Zend_Registry::get('dt_log')->log($this->_ShardsMapping, Zend_Log::DEBUG);


	} //end __construct
        
        
    
    
    
    public function getDb($module,$controller = '',$key) {
    	
    	if(empty($key)) {
            	require_once 'Myzen/Db/Exception.php';
            	throw new Myzen_Db_Exception('getDb should not be used when the key is unknown for module:'.$module.' and controller:'.$controller);    			
    	}
    	
    	//We retrieve the shardId That should be used
    	
    	//We check the number of the shard that should be used
    	$shardAreaNum = $this->_getShardAreaNum($module,$controller);
    	
    	//Now inside the shard we search for the identifier of the database
    	$shardAreaDbNum = $this->_getDbNum($shardAreaNum,$key);
    	
    	//We check if the db has already been called in the past or not
    	if(!empty($this->_loadedDbs[$shardAreaNum][$shardAreaDbNum])) return $this->_loadedDbs[$shardAreaNum][$shardAreaDbNum];
    	
  		$dbAdapter = $this->_loadDb($shardAreaNum,$shardAreaDbNum);
    	
    	//We return the dbAdapter
    	return $dbAdapter;
    	
    }    

	public function getAllDbs($module,$controller) {
    	//We retrieve the shardId That should be used
    	//We check the number of the shard that should be used
    	$shardAreaNum = $this->_getShardAreaNum($module,$controller);
    	
    	//Now we look the number of databases inside the shard
    	$nbDb = $this->_getNbDb($shardAreaNum);
    	
    	//We load the db in an array
    	$dbList = array();
    	for($i = 0;$i < $nbDb;$i++) {
    		if(!empty($this->_loadedDbs[$shardAreaNum][$shardAreaDbNum])) $dbList[] = $this->_loadedDbs[$shardAreaNum][$i];
  			$dbList[] = $this->_loadDb($shardAreaNum,$i);
    	}
    	//We return the dbAdapter
    	return $dbList;	
	}
	
	protected function _loadDb($shardAreaNum,$shardAreaDbNum) {
	    	Zend_Registry::get('dt_log')->log('Loading db shardAreaNum:'.$shardAreaNum.' shardAreaDbNum:'.$shardAreaDbNum, Zend_Log::DEBUG);
    	
    	//The db has never been called so we set it
    	$dbAdapter = Zend_Db::factory($this->_shardsTopology[$shardAreaNum][$shardAreaDbNum]['adapter'],$this->_shardsTopology[$shardAreaNum][$shardAreaDbNum]['params']);
    	
    	//If we need to profile the queries we need to turn on the profiler
    	//We can profile whole shards or a particular shard
    	if($this->_profileEnabled) {
    		if('firebug' == $this->_profilerType) {
	    		$dbAdapter->setProfiler(new Zend_Db_Profiler_Firebug('Profiling shard:'.$shardAreaNum.' DbNum:'.$shardAreaDbNum));
	    		$dbAdapter->getProfiler()->setEnabled(true);
    		}
    		elseif('log' == $this->_profilerType) {
	    		$dbAdapter->setProfiler(new Myzen_Db_Profiler_Log(Zend_Registry::get('dt_log')));
	    		$dbAdapter->getProfiler()->setEnabled(true);
    		}

    	}
    	
    	//We store it in the loaded
    	$this->_loadedDbs[$shardAreaNum][$shardAreaDbNum] = $dbAdapter;	
    	
    	return $dbAdapter;
	}

    
    
    protected function _getDbNum($shardAreaNum,$key) {

    	if(empty($key)) {
            require_once 'Myzen/Db/Exception.php';
            throw new Myzen_Db_Exception('getDb should not be used when the key is unknown for module:'.$module.' and controller:'.$controller.'. If no shqrding possible, use key');    			
    	}
    	
    	//we retrieve the number of Dbs in the shard
    	$nbDb = $this->_getNbDb($shardAreaNum);
    	
    	
    	//We convert the key in a pseudo random number (for a key, the same number will always be returned)
    	$num = substr(base_convert(sha1($key), 16, 10),4,6);
    	
    	//We return the correct number for the db in the shard to use
    	return $num%$nbDb;
	}

	//return the number of databases that compose a shard area
	protected function _getNbDb($shardAreaNum) {
		return count($this->_shardsTopology[$shardAreaNum]);
	}

	
	//public version of the get db num
	//usefull when needed to push the same update in multiple databases
	public function getDbNum($module,$controller = '',$key) {
    	//We check the number of the shard that should be used
    	$shardAreaNum = $this->_getShardAreaNum($module,$controller);
    	//Now inside the shard we search for the identifier of the database
    	return $this->_getDbNum($shardAreaNum,$key);		
	}
	
	protected function _getShardAreaNum($module,$controller) {
		if(isset($this->_ShardsMapping[$module][$controller])) return $this->_ShardsMapping[$module][$controller];
		elseif(isset($this->_ShardsMapping[$module]['*'])) return $this->_ShardsMapping[$module]['*'];
		else return 0;
	}
    
    public function __toString() {
    	return 'Myzen_db_Shard';	
    }
    
}